const hmacSHA256 = require('crypto-js/hmac-sha256')

export default abstract class SignerBase {
  // SHA256 hash of an empty request body
  public readonly EMPTY_BODY_SHA256 =
    'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'

  public readonly SCHEME = 'AWS4'
  public readonly ALGORITHM = 'HMAC-SHA256'
  public readonly TERMINATOR = 'aws4_request'

  // format strings for the date/time and date stamps required during signing
  public readonly ISO8601BasicFormat = 'yyyyMMddThhmmssZ'
  public readonly DateStringFormat = 'yyyyMMdd'

  // some common x-amz-* parameters
  public readonly X_Amz_Algorithm = 'X-Amz-Algorithm'
  public readonly X_Amz_Credential = 'X-Amz-Credential'
  public readonly X_Amz_SignedHeaders = 'X-Amz-SignedHeaders'
  public readonly X_Amz_Date = 'X-Amz-Date'
  public readonly X_Amz_Signature = 'X-Amz-Signature'
  public readonly X_Amz_Expires = 'X-Amz-Expires'
  public readonly X_Amz_Content_SHA256 = 'X-Amz-Content-SHA256'
  public readonly X_Amz_Decoded_Content_Length = 'X-Amz-Decoded-Content-Length'
  public readonly X_Amz_Meta_UUID = 'X-Amz-Meta-UUID'

  // the name of the keyed hash algorithm used in signing
  public readonly HMACSHA256 = 'HMACSHA256'

  // request canonicalization requires multiple whitespace compression
  protected static readonly CompressWhitespaceRegex = /\\s+/

  // algorithm used to hash the canonical request that is supplied to
  // the signature computation
  public static CanonicalRequestHashAlgorithm = hmacSHA256

  constructor(
    public EndpointUri: URL,
    public HttpMethod: string,
    public Service: string,
    public Region: string
  ) {}

  /// <summary>
  /// Returns the canonical collection of header names that will be included in
  /// the signature. For AWS4, all header names must be included in the process
  /// in sorted canonicalized order.
  /// </summary>
  /// <param name="headers">
  /// The set of header names and values that will be sent with the request
  /// </param>
  /// <returns>
  /// The set of header names canonicalized to a flattened, ;-delimited string
  /// </returns>
  protected CanonicalizeHeaderNames(headers: Map<string, string>): string {
    let headersToSign = [...headers.keys()].map((i) => i.toLowerCase())
    headersToSign.sort((a, b) => (a < b ? -1 : 1))

    return headersToSign.join(';')
  }

  /// <summary>
  /// Computes the canonical headers with values for the request.
  /// For AWS4, all headers must be included in the signing process.
  /// </summary>
  /// <param name="headers">The set of headers to be encoded</param>
  /// <returns>Canonicalized string of headers with values</returns>
  protected CanonicalizeHeaders(headers: Map<string, string>): string {
    if (headers.size == 0) return ''

    // step1: sort the headers into lower-case format; we create a new
    // map to ensure we can do a subsequent key lookup using a lower-case
    // key regardless of how 'headers' was created.
    let sortedHeaderKey = [...headers.keys()]
    sortedHeaderKey.sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1))

    // step2: form the canonical header:value entries in sorted order.
    // Multiple white spaces in the values should be compressed to a single
    // space.
    let str = ''
    sortedHeaderKey.forEach((item) => {
      let headerValue = headers
        .get(item)!
        .replace(SignerBase.CompressWhitespaceRegex, ' ')
      str += `${item.toLowerCase()}:${headerValue.trim()}\n`
    })

    return str
  }

  /// <summary>
  /// Returns the canonical request string to go into the signer process; this
  /// consists of several canonical sub-parts.
  /// </summary>
  /// <param name="endpointUri"></param>
  /// <param name="httpMethod"></param>
  /// <param name="queryParameters"></param>
  /// <param name="canonicalizedHeaderNames">
  /// The set of header names to be included in the signature, formatted as a flattened, ;-delimited string
  /// </param>
  /// <param name="canonicalizedHeaders">
  /// </param>
  /// <param name="bodyHash">
  /// Precomputed SHA256 hash of the request body content. For chunked encoding this
  /// should be the fixed string ''.
  /// </param>
  /// <returns>String representing the canonicalized request for signing</returns>
  protected CanonicalizeRequest(
    endpointUri: URL,
    httpMethod: string,
    queryParameters: string,
    canonicalizedHeaderNames: string,
    canonicalizedHeaders: string,
    bodyHash: string
  ): string {
    return `${httpMethod}
${SignerBase.CanonicalResourcePath(endpointUri)}
${queryParameters}
${canonicalizedHeaders}
${canonicalizedHeaderNames}
${bodyHash}`
  }

  /// <summary>
  /// Returns the canonicalized resource path for the service endpoint
  /// </summary>
  /// <param name="endpointUri">Endpoint to the service/resource</param>
  /// <returns>Canonicalized resource path for the endpoint</returns>
  protected static CanonicalResourcePath(endpointUri: URL): string {
    let absolutePath = endpointUri.pathname
    return absolutePath ? encodeURI(absolutePath) : '/'
  }

  /// <summary>
  /// Compute and return the multi-stage signing key for the request.
  /// </summary>
  /// <param name="algorithm">Hashing algorithm to use</param>
  /// <param name="awsSecretAccessKey">The clear-text AWS secret key</param>
  /// <param name="region">The region in which the service request will be processed</param>
  /// <param name="date">Date of the request, in yyyyMMdd format</param>
  /// <param name="service">The name of the service being called by the request</param>
  /// <returns>Computed signing key</returns>
  protected DeriveSigningKey(awsSecretAccessKey: string, date: string): any {
    return hmacSHA256(
      this.TERMINATOR,
      hmacSHA256(
        this.Service,
        hmacSHA256(
          this.Region,
          hmacSHA256(date, this.SCHEME + awsSecretAccessKey)
        )
      )
    )
  }
}
